<?php

/**
 * Ganglib
 *
 * LICENSE
 *
 * This source file is subject to the GNU Lesser Generak Public license.
 * 
 * It is available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/lgpl.html
 *
 * @category    Ganglib
 * @package    Div_Db
 * @copyright  Copyright (c) 2008 Ganglib project members (http://www.code.google.com/p/ganglib)
 * @license     http://www.gnu.org/licenses/lgpl.html
 */

require_once('Div/Db/Query/Dialect/Interface.php');

/**
 * Implementation of SQL query builder
 * 
 * It allows user to build query using PHP object notation (like LINQ ;)). Query could be
 * then translated to SQL string and executed on specified DBMS. SQL Dialect could be changed using
 * specified Div_Db_Query_Dialect_Interface implementation.
 * 
 * Based on: Kohana::Database
 * 
 * @package    Div_Db
 * @copyright  Copyright (c) 2008 Ganglib project members (http://www.code.google.com/p/ganglib)
 * @license     http://www.gnu.org/licenses/lgpl.html 
 */
class Div_Db_Query {
	
	
	/**
	 * User could set SQL text - then it's executed directly to rdbms
	 */
	protected $plain_query = null;
	
	/**
	 * Params assigned to plain query - will be attached to query by specified strategy
	 */
	protected $plain_params = null;

	/**
	 * Dialect used to translate this query to sql string
	 */
	protected $dialect = null;
	
	/**
	 * Configuration of the query
	 */
	protected $config;
	
	
	// Escaped query model - it's used parallel with $query_model for better performance
	protected $query_model_escaped = array(   'select'     => array(),
										    	'set'        => array(),
										  		'from'       => array(),
										  		'join'       => array(),
										  		'where'      => array(),
										  		'orderby'    => array(),
										  		'order'      => array(),
										  		'groupby'    => array(),
										  		'having'     => array(),
										  		'distinct'   => FALSE,
										  		'limit'      => FALSE,
										  		'offset'     => FALSE,
											);
	
	// Raw query data which could be used to partition query across 
	// multiple servers
	protected $query_model			 = array(	'type'	   => null,
												'from'    => array(),
												'join'	   => array(),
								  	  			'where'   => array(),
								  				'orderby' => array(),
								  				'groupby' => array(),
								  				'limit'   => 0,
								  				'offset'  => 0,
								  				'select'  => array(),
								  				'set'	   => array()	
											);
	
	/**
	 * 0 - select, 1 - update, 2 - insert, 3 - delete
	 */	
	protected $query_type = 0;
	protected $last_query = '';
		
	const QUERY_SELECT = 0;
	const QUERY_UPDATE = 1;
	const QUERY_INSERT = 2;
	const QUERY_DELETE = 3;

	const ARRAY_PRECOMPILED = 0;
	const ARRAY_MODEL		 = 1;
	/**
	 * Construct this query builder
	 * 
	 * @param bool $default_dialect should I create default Dialect for You?
	 * @param Zend_Db_Query_Dialect_Interface $dialect database dialect
	 */
	public function __construct($default_dialect = true, Div_Db_Query_Dialect_Interface $dialect = null)
	{
		if($dialect == null && $default_dialect)
		{
			$dialect = $this->defaultDialect();
		}
	
		$this->dialect = $dialect;
	}
	

	/**
	 * Execute plain text query
	 * 
	 * @param string $text SQL query compilant with desired sql rdbms
	 * @param array $model query model if we want to use partitioning
	 * @param array $bindings query parameters bindings - array('key' => 'value') where in sql query we have: SELECT ... FROM ... WHERE param < :key (:key will be replaced with value)
	 * @return this
	 */
	public function plain($text, array $bindings = null, array $model = null)
	{
		$this->plain_query = $text;
		
		if($model)
			$this->query_model = $model;
			
		if($bindings)
			$this->plain_params = $bindings;
			
		return $this;
	}
	

	/**
	 * If dialect is set, escape query part
	 * 
	 * @param string $part query part
	 */
	protected function escapeQueryPart($part, $val)
	{
		if($this->dialect)
		{
			switch($part)
			{
				case 'select':
				{
					if (strpos($val, '(') === FALSE AND $val !== '*')
					{
//						$val = (strpos($val, '.') !== FALSE) ? $this->config->table_prefix.$val : $val;
						if($this->dialect)
							$val = $this->dialect->escape_column($val);
					}
					$this->query_model_escaped['select'][] = $val;
										
				} break;
				
				case 'from':
				{
					$this->query_model_escaped['from'][] = $this->dialect->escape_column(/*$this->config->table_prefix.*/$val);
				} break;
				
				case 'join':
				{
					$this->query_model_escaped['join'][] = $val[0].'JOIN '.$this->dialect->escape_column(/*$this->config->table_prefix.*/$val[1]).' ON '.$val[2];
				} break;
				
				case 'where':
				{
					$this->query_model_escaped['where'][] = $this->dialect->where($val[0], $val[1], $val[2], count($this->query_model_escaped['where']), $val[3]);	
				}break;
				
				case 'like':
				{
					$this->query_model_escaped['where'][] = $this->dialect->like($val[0], $val[1], 'AND ', count($this->query_model_escaped['where']));
				}break;
				
				case 'notlike':
				{
					$this->query_model_escaped['where'][] = $this->dialect->notlike($val[0], $val[1], 'AND ', count($this->query_model_escaped['where']));
				}break;
				
				case 'in':
				{
					$this->query_model_escaped['where'][] = $this->dialect->in($val[0], $val[1], 'AND ', count($this->query_model_escaped['where']));
				}break;

				case 'orin':
				{
					$this->query_model_escaped['where'][] = $this->dialect->in($val[0], $val[1], 'OR ', count($this->query_model_escaped['where']));
				}break;

				case 'notin':
				{
					$this->query_model_escaped['where'][] = $this->dialect->notin($val[0], $val[1], 'AND ', count($this->query_model_escaped['where']));
				}break;
								

				case 'regex':
				{
					$this->query_model_escaped['where'][] = $this->dialect->regex($val[0], $val[1], 'AND ', count($this->query_model_escaped['where']));
				}break;
				
				case 'notregex':
				{
					$this->query_model_escaped['where'][] = $this->dialect->notregex($val[0], $val[1], 'AND ', count($this->query_model_escaped['where']));
				}break;				
				
				case 'groupby':
				{
					$this->query_model_escaped['groupby'][] = $val;
				}break;
				
				case 'having':
				{
					$this->query_model_escaped['having'] = array_merge($this->query_model_escaped['having'], $this->dialect->having($val[0], $val[1], $val[2]));
				} break;
				
				case 'orderby':
				{
					$this->query_model_escaped['orderby'][] = implode(',', $val[0]).$val[1];
				} break;
				
				case 'limit':
				{
					if(is_array($val))
					{
						$this->query_model_escaped['limit']  = (int) $val[0];
						$this->query_model_escaped['offset'] = (int) $val[1];
					} else
						$this->query_model_escaped['limit']  = $val;					
				} break;

				case 'offset':
				{
					$this->query_model_escaped['offset'] = (int) $val;					
				} break;
				
				case 'set':
				{
					$this->query_model_escaped['set'][$val[0]] = $this->dialect->escape($val[1]);
				} break;
								
			}
		}
	}
	
	/**
	 * Return default dialect if none was selected
	 * 
	 * @return Div_Db_Query_Dialect_Interface dialect
	 */
	protected function defaultDialect()
	{
		require_once('Div/Db/Query/Dialect/Mysql.php');
		return Div_Db_Query_Dialect_Mysql::getInstance();
	}

	/**
	 * Build SELECT part of query
	 * 
	 * @param string/array $sql fields You wan't to select from database
	 * @return $this chain for next parts of query
	 */
	public function select($sql = '*')
	{
		$this->query_type = Div_Db_Query::QUERY_SELECT;
				
		if (func_num_args() > 1)
		{
			$sql = func_get_args();
		}
		elseif (is_string($sql))
		{
			$sql = explode(',', $sql);
		}
		else
		{
			$sql = (array) $sql;
		}

		foreach($sql as $val)
		{
			if (($val = trim($val)) == '') continue;

			$this->escapeQueryPart('select', $val);
			$this->query_model['select'][] = $val;
		}

		return $this;
	}


	/**
	 * Build FROM part of query
	 * @param $sql tables You want to use in FROM clause
	 * @return $this chain for next operations
	 */
	public function from($sql)
	{
		foreach((array) $sql as $val)
		{
			if (($val = trim($val)) == '') continue;

			$this->escapeQueryPart('from', $val);
			$this->query_model['from'][] = $val;
		}

		return $this;
	}


	/**
	 * Build JOIN part of query
	 * 
	 * @param string $table table You want to join
	 * @param string $cond joining condition
	 * @param string $type  LEFT, RIGHT, OUTER, INNER, LEFT OUTER...
	 * @return $this chain for next parts of query
	 */
	public function join($table, $cond, $type = '')
	{
		if ($type != '')
		{
			$type = strtoupper(trim($type));

			if ( ! in_array($type, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'), TRUE))
			{
				$type = '';
			}
			else
			{
				$type .= ' ';
			}
		}

		/*if (preg_match('/([a-z.].+)\s+=\s+([a-z.].+)/i', $cond, $where))
		{
			$cond = $this->dialect->escape_column($this->config->table_prefix.$where[1]).
			        ' = '.
			        $this->dialect->escape_column($this->config->table_prefix.$where[2]);
		}*/

		$this->escapeQueryPart('join', array($type, $table, $cond));
		$this->query_model['from'][] = $table;
		$this->query_model['join'][$table] = array('type' => $type, 'cond' => $cond);

		return $this;
	}

	/**
	 * Build WHERE part of query
	 * 
	 * @param array $key array of key=>value for where clause
	 * @param string $value if $key is not an array is used to build WHERE condition: $key = $value
	 * @param bool $quote if variable should be qutoed
	 * @return $this chain for next parts of query 
	 */
	public function where($key, $value = NULL, $quote = TRUE)
	{
		$quote = (func_num_args() < 2 AND ! is_array($key)) ? -1 : $quote;
		$keys  = is_array($key) ? $key : array($key => $value);

		foreach ($keys as $key => $value)
		{
//			$key           = (strpos($key, '.') !== FALSE) ? $this->config->table_prefix.$key : $key;
			if($key /*&& ($value !== NULL)*/)
			{
				$this->escapeQueryPart('where', array($key, $value, 'AND ', $quote));
				$this->modelAddWhere($key, $value);
			}
		}
		return $this;
	}

	
	/**
	 * Add WHERE clause element to query model
	 * Add WHERE clause element to query model
	 * 
	 * @param string $key field
	 * @param string $value field value
	 */
	protected function modelAddWhere($key, $value, $prefix = 'AND')
	{
		$oldValue = null;
		$operator = '';

		if(strpos($key, '<='))
		{
			$operator = '<=';
			$key = str_replace('<=', '', $key);
		} else
		if(strpos($key, '>='))
		{
			$operator = '>=';
			$key = str_replace('>=', '', $key);
		} else
		if(strpos($key, '<'))
		{
			$operator = '<';
			$key = str_replace('<', '', $key);
		} else
		if(strpos($key, '>'))
		{
			$operator = '>';
			$key = str_replace('>', '', $key);
		} else
		if(strpos($key, '='))
		{
			$operator = '';
			$key = str_replace('=', '', $key);
		}
		
		$key = trim($key);
		if(isset($this->query_model['where'][$key]))
			$oldValue =$this->query_model['where'][$key];
		
		if(!$oldValue)
			$this->query_model['where'][$key] = array('value' => $operator.$value, 'prefix' => $prefix);
		else
		{
			if(!isset($oldValue[0]))
				$this->query_model['where'][$key] = array($oldValue, array('value' => $operator.$value, 'prefix' => $prefix));
			else
				$this->query_model['where'][$key][] = array('value' => $operator.$value, 'prefix' => $prefix);
		}
	}

	/**
	 * Same as where() but also adds OR pre condition
	 * 
	 * @param array $key array of key=>value for where clause
	 * @param string $value if $key is not an array is used to build WHERE condition: $key = $value
	 * @param bool $quote if variable should be qutoed
	 * @return $this chain for next parts of query 
	 */
	public function orwhere($key, $value = NULL, $quote = TRUE)
	{
		$quote = (func_num_args() < 2 AND ! is_array($key)) ? -1 : $quote;
		$keys  = is_array($key) ? $key : array($key => $value);

		foreach ($keys as $key => $value)
		{
			//$key           = (strpos($key, '.') !== FALSE) ? $this->config->table_prefix.$key : $key;
			if($key/* && ($value !== NULL)*/)
			{
				$this->escapeQueryPart('where', array($key, $value, 'OR ', $quote));
				$this->modelAddWhere($key, $value, 'OR');
			}
		}

		return $this;
	}

	/**
	 * Add LIKE ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function like($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
//			$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
			if($field && $match)
			{
				$this->escapeQueryPart('like', array($field, $match, 'AND '));
				$this->modelAddWhere($field, $match, 'LIKE');
			}
		}

		return $this;
	}

	/**
	 * Add OR LIKE ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function orlike($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{

			//$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('like', array($field, $match, 'OR '));
				$this->modelAddWhere($field, $match, 'OR LIKE');
			}
		}

		return $this;
	}

	/**
	 * Add AND NOT LIKE ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function notlike($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{
			
			//	$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('notlike', array($field, $match, 'AND'));				
				$this->modelAddWhere($field, $match, 'AND NOT LIKE');
			}
		}

		return $this;
	}
	
	/**
	 * Add WHERE AND $field IN $values
	 * 
	 * @param string $field name
	 * @param mixed $values values or simple subquery?
	 */
	public function in($field, $values)
	{
		if($field)
		{
			$this->escapeQueryPart('in', array($field, $values, 'AND'));				
			$this->modelAddWhere($field, $values, 'AND IN');
		}
		return $this;
	}
	
	/**
	 * Add WHERE OR $field IN $values
	 * 
	 * @param string $field name
	 * @param mixed $values values or simple subquery?
	 */
	public function orin($field, $values)
	{
		if($field)
		{
			$this->escapeQueryPart('orin', array($field, $values, 'OR'));				
			$this->modelAddWhere($field, $values, 'OR IN');
		}
		return $this;
	}	

	/**
	 * Add WHERE AND NOT $field IN $values
	 * 
	 * @param string $field name
	 * @param mixed $values values or simple subquery?
	 */
	public function notin($field,  $values)
	{
		if($field)
		{
			$this->escapeQueryPart('notin', array($field, $values, 'AND'));				
			$this->modelAddWhere($field, $values, 'AND NOT IN');
		}
		return $this;
	}


	/**
	 * Add OR NOT LIKE ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function ornotlike($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{
			
				//$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('notlike', array($field, $match, 'OR '));				
				$this->modelAddWhere($field, $match, 'OR NOT LIKE');
			}
		}

		return $this;
	}

	/**
	 * Add REGEX ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function regex($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{
			
				//$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('regex', array($field, $match, 'AND '));	
				$this->modelAddWhere($field, $match, 'AND REGEX');
			}
		}

		return $this;
	}

	/**
	 * Add OR REGEX ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function orregex($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{
				//$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('regex', array($field, $match, 'OR '));
				$this->modelAddWhere($field, $match, 'OR REGEX');
			}
		}

		return $this;
	}

	/**
	 * Add AND NOT REGEX LIKE ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function notregex($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{
			
				//$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('notregex', array($field, $match, 'AND '));				
				$this->modelAddWhere($field, $match, 'AND NOT REGEX');
			}
		}

		return $this;
	}

	/**
	 * Add OR NOT REGEX ... clause to WHERE
	 * 
	 * @param string $field table field
	 * @param string $match match
	 * @return $this chain for next parts of query
	 */
	public function ornotregex($field, $match = '')
	{
		$fields = is_array($field) ? $field : array($field => $match);

		foreach ($fields as $field => $match)
		{
			if($field && $match)
			{
			
				//$field         = (strpos($field, '.') !== FALSE) ? $this->config->table_prefix.$field : $field;
				$this->escapeQueryPart('notregex', array($field, $match, 'OR '));				
				$this->modelAddWhere($field, $match, 'OR NOT REGEX');
			}
		}

		return $this;
	}

	/**
	 * Add GROUP BY clause
	 * 
	 * @param string $by group by columns
	 * @return $this chain for next parts of query
	 */
	public function groupby($by)
	{
		if ( ! is_array($by))
		{
			$by = explode(',', (string) $by);
		}

		foreach ($by as $val)
		{
			$val = trim($val);

			if ($val != '')
			{
				$this->escapeQueryPart('groupby', $val);
				$this->query_model['groupby'][] = $val;
			}
		}

		return $this;
	}

	/**
	 * Add HAVING clause
	 * 
	 * @param string $key group by
	 * @param string $value having by condition
	 * @return $this chain for next parts of query
	 */
	public function having($key, $value = '')
	{
    	if($key && ($value !==''))
    	{
	    	$this->escapeQueryPart('having', array($key, $value, 'AND'));
	    	$this->query_model['having'][$key] = array('value' => $value, 'prefix' => 'AND');
    	}
		return $this;
	}

	/**
	 * Add OR HAVING clause
	 * 
	 * @param string $key group by
	 * @param string $value having by condition
	 * @return $this chain for next parts of query
	 */
	public function orhaving($key, $value = '')
	{
		if($key && ($value!==''))
		{
	    	$this->escapeQueryPart('having', array($key, $value, 'OR'));		
	    	$this->query_model['having'][$key] = array('value' => $value, 'prefix' => 'OR');
		}
		return $this;
	}

	/**
	 * Add ORDER BY clause
	 * 
	 * @param string $order by columns to order by
	 * @param string $direction ASC or DESC
	 * @return $this chain for next parts of query
	 */
	public function orderby($orderby, $direction = '')
	{
		$direction = strtoupper(trim($direction));

		if ($direction != '')
		{
			$direction = (in_array($direction, array('ASC', 'DESC', 'RAND()', 'NULL'))) ? ' '.$direction : ' ASC';
		}

		if ( ! is_array($orderby))
		{
			$orderby = explode(',', (string) $orderby);
		}

		$order = array();
		foreach ($orderby as $field)
		{
			$field = trim($field);

			if ($field != '')
			{
				//if($this->dialect)
					//$order[] = $this->dialect->escape_column($field);

				if($this->dialect)
					$order[] = $this->dialect->escape($field, false); // because it's possible to use functions in ORDER BY for example FIELD() function
						
				$this->query_model['orderby'][$field] = $direction;
			}
		}
		
		if(count($order)>0)
			$this->escapeQueryPart('orderby', array($order, $direction));
			
		return $this;
	}

	/**
	 * Add LIMIT clause
	 * 
	 * @param int $limit start from
	 * @param int $offset how many records read,
	 * @return $this chain for next parts of query
	 */
	public function limit($limit, $offset = FALSE)
	{
		if($offset != FALSE)
		{
			$this->escapeQueryPart('limit', array((int)$limit, (int)$offset));		
			$this->query_model['limit'] = (int)$limit;
			$this->query_model['offset'] = (int)$offset;
		} else
		{
			$this->escapeQueryPart('limit', (int)$limit);
			$this->query_model['limit'] = (int)$limit;
		}
		return $this;
	}

	/**
	 * Add OFFSET clause
	 * 
	 * @param int $value offset value
	 * @return $this chain for next parts of query
	 */
	public function offset($value)
	{
		$this->escapeQueryPart('offset', (int)$value);
		$this->query_model['offset'] = (int)$value;
		return $this;
	}

	/**
	 * Add SET clause to UPDATE or INSERT queries
	 * 
	 * @param string $key value to update could be an array
	 * @param string $value to set
	 * @return $this chain for next parts of query
	 */
	public function set($key, $value = '')
	{
		if ( ! is_array($key))
		{
			$key = array($key => $value);
		}

		foreach ($key as $k => $v)
		{
			if($k)
			{
				$this->escapeQueryPart('set', array($k, $v));
				$this->query_model['set'][$k] = $v;
			}
		}

		return $this;
	}

	/**
	 * Reset SELECT to use this object once again
	 */
	private function reset_select()
	{
		$this->query_model_escaped['select']   = array();
		$this->query_model_escaped['from']     = array();
		$this->query_model_escaped['join']     = array();
		$this->query_model_escaped['where']    = array();
		$this->query_model_escaped['orderby']  = array();
		$this->query_model_escaped['groupby']  = array();
		$this->query_model_escaped['having']   = array();
		$this->query_model_escaped['distinct'] = FALSE;
		$this->query_model_escaped['limit']    = FALSE;
		$this->query_model_escaped['offset']   = FALSE;
		$this->query_model = 	array(	'type'	   => null,
										'from'    => array(),
								  	  	'where'   => array(),
								  		'orderby' => array(),
								  		'groupby' => array(),
								  		'join'	   => array(),
								  		'limit'   => 0,
								  		'offset'  => 0,
								  		'select'  => array(),
								  		'set'	   => array()	
								);	
	}

	/**
	 * Reset UPDATE/INSERT/DELETE to use this object once again
	 */
	private function reset_write()
	{
		$this->query_model_escaped['set']   = array();
		$this->query_model_escaped['from']  = array();
		$this->query_model_escaped['where'] = array();
		$this->query_model = 	array(	'from'    => array(),
								  	  	'where'   => array(),
								  		'set'	   => array()	
								);
	}


	/**
	 * Escape value to safe use in database
	 * 
	 * @param string $value value to escape
	 * @return string escaped value
	 */
	public function escape($value)
	{
		return $this->dialect->escape($value);
	}

	/**
	 * Escape value in string manner
	 * 
	 * @param string $str string to escape
	 * @return string escaped string
	 */
	public function escape_str($str)
	{
		return $this->dialect->escape_str($str);
	}
	
	/**
	 * Compile statement to string
	 * 
	 * @return string compiled query
	 */
	public function __toString()
	{
		if($this->plain_query)
			return $this->plain_query; // user sets plain text!			
			
		//die(print_r($this->toArray($this),true));
		if(!$this->dialect)
			self :: throwException('Please set Dialect before compile query, or uset toArray() method to get query model');
		
		switch($this->query_type)
		{
			case Div_Db_Query::QUERY_SELECT: $this->last_query = $this->dialect->compile_select($this->toArray(Div_Db_Query::ARRAY_PRECOMPILED)); break;
			case Div_Db_Query::QUERY_UPDATE: $this->last_query = $this->dialect->compile_update($this->toArray(Div_Db_Query::ARRAY_PRECOMPILED)); break;
			case Div_Db_Query::QUERY_INSERT: $this->last_query = $this->dialect->compile_insert($this->toArray(Div_Db_Query::ARRAY_PRECOMPILED)); break;
			case Div_Db_Query::QUERY_DELETE: $this->last_query = $this->dialect->compile_delete($this->toArray(Div_Db_Query::ARRAY_PRECOMPILED)); break;
		}
		
		return $this->last_query;		
	}
	
	/**
	 * Return lastly generated query
	 * 
	 * @return string
	 */
	public function getLastQuery()
	{
		return $this->last_query;
	}
	
	/**
	 * Return parts of query as array for processing
	 * 
	 * @param string $mode = Div_Db_Query::ARRAY_PRECOMPILED - precompiled query parts or $mode = Div_Db_Query::ARRAY_MODEL - returns query model, which could be used for partitioning for example
	 * @return array ('select' =>, 'set' =>, 'from' =>, 'join' =>, 'where' =>, 'orderby' =>, 'order' =>, 'groupby' =>, 'having' =>, 'distinct' =>, 'limit' =>, 'offset' =>)
	 */
	public function toArray($mode = Div_Db_Query::ARRAY_PRECOMPILED)
	{
		$result = array();
		
		if($mode == Div_Db_Query::ARRAY_PRECOMPILED)
		{
			$result = $this->query_model_escaped;
		}

		if($mode == Div_Db_Query::ARRAY_MODEL)
		{
			$result = $this->toModelArray();
		}
		
		return $result;
	}
	
	/**
	 * Return query model. Query model is a set of uncompiled query parts. Depends
	 * on model we could for example partition our database queries.
	 */
	public function toModelArray()
	{
		$this->query_model['type'] = $this->query_type;
		return $this->query_model;		
	}

	/**
	 * Create UPDATE part of query. Fields to update should be setted by $thi->set()
	 *
	 * @param string $table table name to update 
	 * @return $this chain for next query operations
	 */	
	public function update($table = '')
	{
		$this->query_type = Div_Db_Query::QUERY_UPDATE;
		
		$this->from($table);

/*		if ($this->set == FALSE)
		{
			return FALSE;
		}*/

		if ($table == '')
		{
			if($this->dialect)
			{
				if ( ! isset($this->query_model_escaped['from'][0]))
				{
					return FALSE;
				}

				$table = $this->query_model_escaped['from'][0];
			} else
				return FALSE;
		}

		return $this;
	}
 
	/**
	 * Create INSERT part of query. Fields to update should be setted by $this->set(), or using $set argument
	 *
	 * @param string $table table name to update
	 * @param array $set values to insert to database 
	 * @return $this chain for next query operations
	 */ 
	public function insert($table = '', $set = NULL)
	{
		$this->query_type = Div_Db_Query::QUERY_INSERT;
		$this->from($table);
		
		if ( ! is_null($set))
		{
			$this->set($set);
		}

		if ($this->query_model_escaped['set'] == NULL)
		{
			return FALSE;
		}
	
		return $this;
	} 
	
	/**
	 * Create DELETE part of query.
	 *
	 * @param string $table table name to delete 
	 * @return $this chain for next query operations
	 */	
	public function delete($table = '')
	{
		$this->query_type = Div_Db_Query::QUERY_DELETE;
		
		if($table) 
			$this->from($table);
		
/*		if (count($this->where) < 1)
		{
			return FALSE;
		}*/

		return $this;	
	} 
	
	/**
	 * Is this query a plain text?
	 * 
	 * @return bool
	 */
	public function isPlain()
	{
		return ($this->plain_query!='');
	}
	
	/**
	 * Return params assigned to query
	 * 
	 * @return array
	 */
	public function plainParams()
	{
		return $this->plain_params;
	}
	
	/**
	 * Resets query parts setted previously
	 * 
	 * @param string $part of query to reset
	 * @return void
	 */
	public function reset($part = null)
	{
		if($this->plain_query)
			$this->plain_query = null;
		
		if($part == null)
		{
			if($this->query_type == Div_Db_Query::QUERY_SELECT)
				$this->reset_select();
			else
				$this->reset_write();
		} else
		{
			$default_value =  ($part!='limit' && $part!='offset' && $part!='distinct' && $part!='type')?array():null;
			
			if(isset($this->query_model_escaped[$part]))
			{
				$this->query_model_escaped[$part] = $default_value;
			}
			
			if(isset($this->query_model[$part]))
			{
				$this->query_model[$part] = $default_value;
			}
			
		}
		
	}
	
	
    /**
     * Throw an exception
     *
     * Note : for perf reasons, the "load" of Zend/Cache/Exception is dynamic
     * @param  string $msg  Message for the exception
     * @throws Zend_Cache_Exception
     */
    public static function throwException($msg)
    {
        // For perfs reasons, we use this dynamic inclusion
        require_once 'Div/Db/Query/Exception.php';
        throw new Div_Db_Distributor_Exception($msg);
    }	

} 
